home *** CD-ROM | disk | FTP | other *** search
Text File | 1992-11-23 | 13.9 KB | 459 lines | [ttro/ttxt] |
- Object-oriented programming in AppleScript
- ==========================================
- Warren Harris 11/22/92
-
- Intro
- -----
- You've heard John Scully talk about it, now you can experience it for real:
- AppleScript is an OODL (Object-Oriented Dynamic Language) with which you and
- your friends can perform true object-oriented programming.
-
- What do we mean by object-oriented programming? We mean that AppleScript has
- constructs for bundling handlers and properties together, and a means by which
- messages can be sent to these bundles. We call these bundles "actors." Actors
- are used as both classes and instances in AppleScript. Actors can be sent
- messages by using the "tell" statement.
-
- Here's a quick example demonstrating how actors can be constructed, and how
- they can respond differently to the same message. Let's define two actors,
- John and Jean Luis who respond to the SayHello message differently:
-
- actor "John"
- to SayHello to someone
- return "Hello " & someone
- end SayHello
- end actor
-
- actor "Jean Luis"
- to SayHello to someone
- return "Bonjour " & someone
- end SayHello
- end actor
-
- This defines John and Jean Luis. The actor...end actor construct brackets the
- commands that constitute the definition of each actor. Each of them has a
- single handler definition, SayHello, which takes a parameter describing who
- to greet. Since SayHello is a message name we've made up (rather than one
- which comes from some application's terminology), so we're not allowed to have
- any spaces or special characters in the name. Now if we tell each of them to
- say hello, we see different results returned:
-
- tell actor "John" to SayHello to "Michael"
- --> "Hello Michael"
-
- tell actor "Jean Luis" to SayHello to "Michael"
- --> "Bonjour Michael"
-
-
- Properties
- ----------
- Actors can also contain properties. Just as we define properties at the top
- level of a Toy Surprise file, we can define properties inside of actors. These
- properties have values which persist after the actor is created, and are
- accessible whenever the actor is sent a message.
-
- For example, we can add a counter to one of the above actors as a property:
-
- actor "John"
- property n : 0
-
- to SayHello to someone
- set n to n + 1
- return "Hello " & someone & "(I've told you " & n & " times)"
- end SayHello
- end actor
-
- Now when we send the SayHello message to John, he becomes indignant, complaining
- about the number of times he's had to tell you:
-
- tell actor "John" to SayHello to "Michael"
- --> "Hello Michael (I've told you 1 times)"
-
- tell actor "John" to SayHello to "Michael"
- --> "Hello Michael (I've told you 2 times)"
-
- tell actor "John" to SayHello to "Michael"
- --> "Hello Michael (I've told you 3 times)"
-
- Note however that if you do this in Toy Surprise, each time you reevaluate the
- actor "John" construct it re-initializes the counter n to 0. Therefore if you
- put one tell statement at the bottom every time you run it it will say "1 time"
- whereas if you put two tell statements at the bottom it will always say "2
- times", etc.
-
-
- A note about Toy Surprise
- -------------------------
- Toy Surprise operates by constructing an actor out of the script statements you
- type into each of its windows. It's as if you implicitly added the actor...
- end actor construct to the handlers and commands in the script. This means that
- once a script is compiled its property declarations are persistent. Just for
- fun, try the following script in a Toy Surprise window:
-
- property p : 0
- set p to p+1
- p
-
- When you press the Run button, you'll see what you expect, the result 1.
- However, if you press the Run button again (without touching the script text
- or causing a recompile) you'll get the result 2! Doing this again yields 3!
- What's going on?
-
- Each time you run this script, it increments the persistent property p. P is
- only initialized to 0 when the script is compiled. After that it retains its
- value from the last invocation.
-
- This is great for applets and droplets. They essentially can remember things
- from invocation to invocation. Try this variation as an applet:
-
- property p : 0
- set p to p+1
- display dialog p as string
-
- (Sorry the "as string" is required for display dialog). Each time you run this
- applet it displays the incremented value of p. When it quits it writes itself
- out remembering the last value of p.
-
- Droplets currently have a bug -- they're supposed to stay up after you run them,
- listening for new AppleEvents to respond to. The behavior ends up being the
- same as for applets, but you don't have to put up with the overhead of saving
- things to disk when the process quits, and the startup time the next time it's
- invoked. When you finally send a droplet the Quit message (or switch to it's
- layer and hit Command-Q) it then saves its properties.
-
- With droplets you should be able to write something like:
-
- property p : 0
- on close
- set p to p + 1
- display dialog p as string
- end
-
- This isn't normally what you do with the Close message, but this is just for
- demonstration purposes. Then, if the droplet were saved with the name "doit"
- and from another script we ran:
-
- tell application "doit"
- close
- close
- end
-
- The first Close message would cause the dialog containing the number 1 to be
- displayed, and the second Close message would cause the number 2 to be
- displayed. If you ran this again, 3 and 4 would come up in the dialog box.
- (Remember, don't try this at home. It's broken now. We just wanted to let you
- know where we're going with all this.)
-
- The bottom line: actors, applets and droplets are all "objects" in the object-
- oriented programming sense. You can send them messages whether they're
- in their own application layer (applets and droplets) or whether they're just
- objects in the current program.
-
-
- Classes and Instances
- ---------------------
- Real object-oriented programming is more than just sending messages though.
- Essential to OOP is the notion of classes and instances, and inheritance.
- AppleScript supports both of these concepts. We'll look at classes and
- instances first.
-
- AppleScript doesn't really make a distinction between the things that are
- classes and the things that are instances at runtime -- they're all actors.
- The distinction lies in how actors are used. There are two ways of using
- actors that make them behave like classes: by naming actors, and by writing
- constructor functions. All other uses of actors are to treat them as instances
- -- getting and setting properties (call them slots, instance variables,
- whatever) and sending them messages.
-
- Actors may be named or unnamed. Named actors can be used like classes because
- there is a unique actor associated with a given actor name. This gives a
- convenient way for the actor to be copied in order to construct new instances.
- For example, if we define a Point actor to have an x coordinate of 50, and
- later define it to have an x coordinate of 0, the Point definition is updated:
-
- actor "Point"
- prop x : 50
- prop y : 0
- end
-
- set myPoint to actor "Point"
-
- x of myPoint
- --> 50
-
- actor "Point"
- prop x : 0
- prop y : 0
- end
-
- x of myPoint
- --> 0
-
- Here, myPoint is not reset, but the definition of Point is updated to contain
- the new coordinates (namely the new value of x). Named actors gives a
- convenient way of always finding an actor, no matter what context you're in.
- Instances can be created by copying an actor:
-
- copy actor "Point" to pt1
- set x of pt1 to 33
-
- copy actor "Point" to pt2
- set x of pt2 to 44
-
- Here, we've made two instances of the "class" Point, pt1 and pt2, and then set
- each of their x coordinates. We can tell they're different points by getting
- their x coordinates back out, and seeing that they're different:
-
- x of pt1
- --> 33
-
- x of pt2
- --> 44
-
- Note that we have to use copy to do this. If we use set (i.e. 'set pt1 to
- actor "Point"') we would have just made our local variable pt1 a reference to
- actor "Point", we wouldn't have actually made a copy.
-
- The other way to use actors as classes is to write constructor functions. These
- are functions (procedures, handlers, subroutines, whatever you want to call
- them) that initialize and return actors when they are run. For example:
-
- on makePoint()
- actor
- prop x : 0
- prop y : 0
- end
- end
-
- This defines a function called makePoint that takes no arguments and returns
- and actor with an x and y property. This may look a little strange at first
- (perhaps a little too simple), but here's how it works. When you call this
- function, it always makes a new actor (remember actor definitions construct
- actors, so a new one gets constructed whenever you call this function). Then
- the last result (the new actor) is returned from the function when it
- terminates. (We could have inserted a 'return result' statement after the
- actor definition, but it's not necessary.)
-
- Now we can use this constructor function just like we used the named actor
- "Point", above:
-
- set pt1 to makePoint()
- set x of pt1 to 33
-
- set pt2 to makePoint()
- set x of pt2 to 44
-
- Note that we don't have to use copy here, because calling the function always
- makes a new actor. Calling copy would just copy that new actor again,
- needlessly. We can fetch the individual values of the x coordinates just as
- before:
-
- x of pt1
- --> 33
-
- x of pt2
- --> 44
-
- So why go to all this hassle of making constructor functions? Because this
- allows us to have parameterized classes. Parameterized classes allow us to
- make instances with different initial values to properties:
-
- on makePoint(initialX, initialY)
- actor
- prop x : initialX
- prop y : initialY
- end
- end
-
- We can now construct our two points as:
-
- set pt1 to makePoint(33, 0)
-
- set pt2 to makePoint(44, 0)
-
- and again retrieve their individual x (and y) coordinates as before.
-
-
- Inheritance
- -----------
- Actors support "delegation"-style inheritance. Delegation means that actors
- can forward messages on to a parent actor who may choose to handle the message.
- Actors currently only support single inheritance -- a single parent actor.
-
- The use of delegation-style inheritance works well with our use of actors as
- both classes and instances. Actors being used as classes can "inherit" from
- other classes also being used as classes. Actors being used as instances can
- "delegate" a message to another actor being used as an instance. These are
- essentially the same concept. Actors simply forward messages on to their
- "parent" when they don't handle the message themselves.
-
- Let's begin by showing an example of inheritance. Suppose we define a Stack
- class that we'll use as a base class to inherit from:
-
- actor "Stack"
- prop elements : {}
-
- on push(x)
- set elements to {x} & elements
- return x
- end
-
- on pop()
- set value to top()
- set elements to rest of elements
- return value
- end
-
- on top()
- return item 1 of elements
- end
- end
-
- This class uses a property, elements, which is a list containing the elements
- (we could have used a vector and an index counter, but this will do for now).
-
- Now we can define a subclass by simply mentioning actor "Stack" as the parent
- property of the subclass. We can also add a new method, dup, which duplicates
- the top of the stack:
-
- actor "SuperStack"
-
- prop parent : actor "Stack"
-
- on dup()
- push(top())
- end
- end
-
- Here, SuperStack doesn't define a push or top method, but inherits them from
- Stack. When it is sent the dup message, it sends itself the push and top
- messages which end up being handled by Stack.
-
- Now let's use instances of our SuperStack to see how it works:
-
- copy actor "SuperStack" to s1
- copy actor "SuperStack" to s2
-
- tell s1 to push(3)
- tell s2 to push(4)
- tell s1 to dup()
-
- tell s2 to pop()
- --> 4
-
- tell s1
- {pop(), pop()}
- end
- --> {3, 3}
-
- The trick that makes this work is that when SuperStack is copied, its parent,
- Stack, is also copied. This gives each new instance new copies of all the
- properties. S1 and s2 each get their own copy of the elements property.
-
- Rather than just inheriting methods, we can also override methods -- replace
- them with methods that do something completely different. We can make our
- SuperStack not allow the pop method by giving it a new pop method that
- signals an error:
-
- actor "SuperStack"
-
- prop parent : actor "Stack"
-
- on dup()
- push(top())
- end
-
- on pop()
- error "SuperStack doesn't allow pop"
- end
- end
-
- Now we can perform push, top, and dup on instances of SuperStacks, but not pop:
-
- copy actor "SuperStack" to s3
-
- tell s3 to push(6)
- tell s3 to dup()
- tell s3 to pop()
- --> Error: SuperStack doesn't allow pop
-
-
- Writing "Wrapper" methods
- -------------------------
- Sometimes when doing OOP it is necessary to not just inherit or override
- methods, but to augment their functionality. This can be done by using the
- "continue" statement.
-
- Let's write a CountingStack class which augments the functionality of push and
- pop to keep a count of the number of elements currently on the stack:
-
- actor "CountingStack"
-
- prop parent : actor "Stack"
-
- prop numberOfElements : 0
-
- on push(x)
- set numberOfElements to numberOfElements + 1
- continue push(x)
- end
-
- on pop()
- set numberOfElements to numberOfElements - 1
- continue pop()
- end
- end
-
- Here, continue causes the method to be handed off to the parent, in this case
- the Stack class. Here's an example of it in action:
-
- copy actor "CountingStack" to cs1
-
- tell cs1
- push(33)
- push(44)
- push(55)
- end
-
- numberOfElements of cs1
- --> 3
-
- tell cs1
- pop()
- get its numberOfElements
- end
- --> 2
-
- When continue is called, the arguments passed to the parent may be changed.
- Similarly, any result returned from the parent's method will be returned from
- the continue statement. Just to demonstrate this capability, can make a
- subclass of Stack that (assuming the items pushed are integers) stores them
- internally values as multiplied by 10. Then when popped off the stack, it
- divides them back down to the original value:
-
- actor "PerverseStack"
-
- prop parent : actor "Stack"
-
- on push(x)
- continue push(x * 10)
- end
-
- on pop()
- continue pop()
- return result div 10
- end
- end
-
-
- copy actor "PerverseStack" to ps
-
- tell ps
- push(3)
- push(4)
- return {pop(), pop()}
- end
- -> {4, 3}
-
- But really, this is useful.
-